"""Die Roller

Function that takes a die roll string and returns a value of the roll.
The string should have no extra spaces

Funcitons:
checkroll -- returns a boolean value if the roll string generated errors
rolldice -- returns the value of the die roll
listroll -- returns a list of integers, ignores modifiers
examineroll -- dumps information about a roll to sys.out. Useful for troubleshooting
"""

__author__="Joshua R English"
__date__="$Date: 2014/04/26 16:36:42 $"
__version__="$Revision: 2.1 $"
__copyright__="Copyright (c) 2004, 2009 Josh English"
__license__="Python"

__history__="""
    1.1 Fixed a problem that crashed if the function was given d0 or d1 as input
    1.2 Added the listroll function
    1.3 Added the checkroll function
    2.0 Revamped the system for checking rolls, removing the regular expression
    2.1 Added roll_with_spite, global MINROLL option, adjustroll
    """

__all__=["RollError", "rolldice", "listroll", "checkroll", 'rollwithspite',
    'MINROLL', 'get_minroll', 'set_minroll']

MINROLL = None
def get_minroll():
    global MINROLL
    return MINROLL

def set_minroll(value):
    global MINROLL
    if value in [None, 'None','none']:
        MINROLL = None
        return True
    else:
        value = int(value)
        MINROLL = value
        return True
    raise RollError('Cannot set MINROLL to %s' % value)

from random import randint

class RollError(Exception): pass

import itertools as IT

#~ __parsesroll generates all the errors of checking the roll
#~ so no other function has to do these steps
def parseroll(s):
    n, d, x = s.partition('d')
    if not x:
        raise RollError("Missing 'd' requirement")
    #print n, x
    if not n: n = 1
    try:
        n = int(n)
    except:
        raise RollError("Cannot convert number of dice to integer")
    # x has everything else
    #print "Num:X", n, ":", x
    size = IT.takewhile(lambda ch: ch.isdigit(), x)
    size = ''.join(size)

    #print "size:", size
    x = x[len(size):]
    #print "left with:", x
    adj = IT.takewhile(lambda ch: ch.isdigit(), reversed(x))
    adj = ''.join(adj)
    #print "Adjustment:" , adj, "[%d]" % len(adj)
    if x and not adj:
        raise RollError("Roll has operator but no adjustment")
    if x:
        op, garbage, trash = x.partition(adj)
        #print "op:", op
        if op not in ["", "+", "-", "x", "X"]:
            raise RollError("Bad operator %s" % op)
    else:
        op = ''
        adj = 0
    return int(n), int(size), op, int(adj)


def checkroll(s):
    """checkroll
        Checks if the roll is valid. Returns a boolean value
    """
    try:
        #num,size,opt,adj = pattern.search(s).groups()
        parseroll(s)
        return True
    except:
        return False

def examineroll(s):
    try:
        #num, size, opt, adj = pattern.search(s).groups()
        num, size, opt, adj = parseroll(s)
        print "Roll:", s
        print "Number:", num
        print "Size:", size
        print "Operator:", opt
        print "Adjustment:", adj
        print "Sample result:", rolldice(s)
    except Exception as err:
        print "Bad roll", s
        print "Error:", err, "(%s)"%err.__class__.__name__

def rolldice(s):
    """rolldice(s)
        Roll the appropriate dice and return the value of the roll
        Sample Valid inputs:
            d4 -- Roll one four sided die
            2d4 -- roll two four sided dice
            3d6 -- roll three six sided dice
            4d8-3 -- roll four eightsided dice and subtract 3
            2d10+1 -- roll two ten sided dice and add one
            5d6x3 -- roll five six sided dice and multiply by three
    """

    num, size, opt, adj = parseroll(s)

    total = 0

    try:
        for i in xrange(num): total += randint(1,size)
    except ValueError:
        total = 0

    total = adjustroll(total, opt, adj)

    if MINROLL is not None:
        total = max(total, MINROLL)

    return total

def adjustroll(total, opt, adj):
    """adjustroll(total, opt, adj)
    Adjusts the total according to the operator and adjustment.
    The op and adj arguments should be generated by parseroll
    """
    if opt == '':
        total = total
    elif opt == '+':
        total += adj
    elif opt == '-':
        total -= adj
    elif opt in ['x', 'X']:
        total *= adj
    elif opt == '/':
        total /= adj
    else:
        raise RollException("Bad operation character %s (really bad)" % opt)

    return total

def listroll(s):
    """listroll(s)
        Roll the appropriate dice and returns a list of the dice rolled
        Input is the same as for rolldice, but the modifiers are ignored
    """
    num, size, opt, adj = parseroll(s)

    res = []
    for i in xrange(num): res.append(randint(1,size))

    return res

def rollwithspite(s):
    """rollwithspite(s)
    Rolls the appropriate dice.
    Returns the adjusted total, a list of dice rolled, and the number of dice
    that rolled at the maximum value.
    """
    num, size, opt, adj = parseroll(s)
    rolls = []
    total = 0
    spite = 0
    for i in xrange(num):
        this = randint(1, size)
        rolls.append(this)
        total += this
        if this == size:
            spite += 1

    total = adjustroll(total, opt, adj)


    if MINROLL is not None:
        total = max(total, MINROLL)
    return total, rolls, spite


def take_largest(s, n):
    "Rolls the dice, but only includes the n largest rolls"
    num, size, opt, adj = parseroll(s)

    res = []
    for i in xrange(num):
        res.append(randint(1,size))

    tops = sorted(res, reverse=True)

    total = sum(tops[:n])
    total = adjustroll(total, opt, adj)

    if MINROLL is not None:
        total = max(total, MINROLL)
    return total




if __name__=="__main__":
    pass

